package com.yunkeji.api.call.service.impl;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.ListUtil;
import cn.hutool.core.date.DatePattern;
import cn.hutool.core.date.DateUnit;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.NumberUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.spring.SpringUtil;

import com.baomidou.lock.annotation.Lock4j;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.yunkeji.api.call.bo.*;
import com.yunkeji.api.call.mapper.ApiCallSheetInfoMapper;
import com.yunkeji.api.call.service.CallService;
import com.yunkeji.api.call.vo.*;
import com.yunkeji.api.common.service.SysParamService;
import com.yunkeji.api.user.mapper.ApiAppUserMapper;
import com.yunkeji.api.user.service.AppUserAuthService;
import com.yunkeji.common.constant.CacheConstants;
import com.yunkeji.common.constant.SysConfigConsts;
import com.yunkeji.common.constant.SysIdConstant;
import com.yunkeji.common.core.domain.dto.PayLogDTO;
import com.yunkeji.common.core.domain.event.PushOnlineEvent;
import com.yunkeji.common.enums.app.*;
import com.yunkeji.common.exception.AppServiceException;
import com.yunkeji.common.exception.ServiceException;
import com.yunkeji.common.helper.LoginHelper;
import com.yunkeji.common.msg.PushMessage;
import com.yunkeji.common.msg.bo.CallInfo;
import com.yunkeji.common.msg.consts.MsgConstant;
import com.yunkeji.common.utils.DateUtils;
import com.yunkeji.common.utils.ServletUtils;
import com.yunkeji.common.utils.StringUtils;
import com.yunkeji.common.utils.call.TencentRTCUtils;
import com.yunkeji.common.utils.redis.RedisUtils;
import com.yunkeji.dao.domain.*;
import com.yunkeji.dao.mapper.*;
import com.yunkeji.utils.CallCountUtils;

import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Objects;

import javax.servlet.http.HttpServletRequest;

/**
 * @author 老张
 */
@Service
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
@Slf4j
public class CallServiceImpl implements CallService {

    private final ApiAppUserMapper apiAppUserMapper;
    private final AppUserVipMapper appUserVipMapper;
    private final AppUserWalletMapper appUserWalletMapper;
    private final ApiCallSheetInfoMapper apiCallSheetInfoMapper;
    private final CallRoomMapper callRoomMapper;
    private final AppUserExtMapper appUserExtMapper;
    private final UserLevelMapper userLevelMapper;
    @Autowired SysParamService sysParamService;
    @Autowired @Lazy CallService callService;
    private final UserBlackListMapper userBlackListMapper;
    private final AppUserAuthService appUserAuthService;
    private final CallVoiceCheckMapper callVoiceCheckMapper;

    /**
     * 开始呼叫
     *
     * @author 老张
     * @param startCallBo
     */
    @Override
    @Lock4j(
            acquireTimeout = 10000,
        name = CacheConstants.LOACK_CALL,
            keys = {"#startCallBo.callUid"})
    public CallSheetVo startCall(StartCallBo startCallBo) {

        StartCallType byCode = StartCallType.getByCode(startCallBo.getType());
        Long callUid = LoginHelper.getLoginUser().getUserId();
        AppUser callUser = apiAppUserMapper.selectById(callUid);
        if (byCode.getCode().equals(StartCallType.客服视频.getCode())) {
            if (!callUser.getAccountType().equals(AccountTypeEnums.客服.getCode())) {
                throw AppServiceException.paramException("当前账号暂时无法呼叫");
            }
        }
        if (Objects.equals(callUser.getSex(), SexEnum.女.getCode())){
            String value = sysParamService.selectConfigByKey("female_call_permission");
            if(value.equals("1")){
                throw AppServiceException.paramException("当前账号暂时无法呼叫");
            }
        }

        AppUserExt callUserExt = appUserExtMapper.selectById(callUid);
        AppUser answerUser = apiAppUserMapper.selectById(startCallBo.getUid());
        // 检查通话状态
        checkCallStatus(callUser, answerUser);
        log.info("呼叫：" + callUser.toString());
        log.info("接听" + answerUser.toString());
        // 检查通话余额-返回单价
        CallPayInfo callPayInfo = checkBalance(callUser, answerUser, startCallBo);
        // 创建话单
        CallSheetInfo sheetInfo =
                createCallSheetInfo(callPayInfo, startCallBo, callUser, answerUser);
        // 呼叫日志
        CallLog.builder()
                .createTime(new Date())
                .ucid(sheetInfo.getUcid())
                .type(CallSheetStatus.等待接通.getCode())
                .remark(CallSheetStatus.等待接通.name() + ":开始呼叫")
                .belongUid(LoginHelper.getUserId())
                .callerId(callUser.getId())
                .answerId(answerUser.getId())
                .build()
                .insert();
        // 推送被叫通话信令
        CallOperationEnum.发起呼叫
                .getApply()
                .apply(
                        CallInfo.builder()
                                .type(startCallBo.getType())
                                .toId(answerUser.getId())
                                .formId(callUser.getId())
                                .fromNickName(callUserExt.getNickName())
                                .fromHeadUrl(callUserExt.getHeadUrl())
                                .fromPlatFormId(callUser.getPlatformId())
                                .toPlatFormId(ListUtil.toList(answerUser.getPlatformId()))
                                .ucid(sheetInfo.getUcid())
                                .price(callPayInfo.getUnitPrice())
                                .build());
        // TODO  8.被叫离线创建离线通知
        // TODO 9.只有在线才推送通话信令，避免离线期间收到多次呼叫的问题
        return BeanUtil.toBean(sheetInfo, CallSheetVo.class);
    }

    /**
     * 被叫振铃
     *
     * @author 老张
     * @param bo
     */
    @Override
    @Lock4j(
            acquireTimeout = 10000,
            name = CacheConstants.LOACK_CALL,
            keys = {"#bo.ucid"})
    public void startRinging(CallProcessBo bo) {
        CallSheetInfo callSheetInfo = checkWaitCall(bo.getUcid());
        // 呼叫日志
        CallLog.builder()
                .createTime(new Date())
                .ucid(callSheetInfo.getUcid())
                .type(CallSheetStatus.等待接通.getCode())
                .remark(CallSheetStatus.等待接通.name() + ":被叫振铃")
                .belongUid(LoginHelper.getUserId())
                .callerId(callSheetInfo.getCallerId())
                .answerId(callSheetInfo.getAnswerId())
                .build()
                .insert();
        // 推送主叫通话信令
        CallOperationEnum.被叫振铃
                .getApply()
                .apply(
                        CallInfo.builder()
                                .fromPlatFormId(callSheetInfo.getAnswerPlatformId())
                                .toPlatFormId(ListUtil.toList(callSheetInfo.getCallPlatformId()))
                                .ucid(callSheetInfo.getUcid())
                                .price(callSheetInfo.getCallPrice())
                                .build());
    }

    /**
     * 3.被叫接听-创建聊天房间-返回userSig
     *
     * @author 老张
     * @param bo
     * @return
     */
    @Override
    @Lock4j(
            acquireTimeout = 10000,
            name = CacheConstants.LOACK_CALL,
            keys = {"#bo.ucid"})
    @SneakyThrows
    public StartAnswerVo startAnswer(StartAnswerBo bo) {
        CallSheetInfo callSheetInfo = checkWaitCall(bo.getUcid());
        AppUser callUser = apiAppUserMapper.selectById(callSheetInfo.getCallerId());
        AppUser answerUser = apiAppUserMapper.selectById(callSheetInfo.getAnswerId());

        // 呼叫日志
        CallLog.builder()
                .createTime(new Date())
                .ucid(callSheetInfo.getUcid())
                .type(CallSheetStatus.正在通话.getCode())
                .remark(CallSheetStatus.正在通话.name() + ":被叫接听")
                .belongUid(LoginHelper.getUserId())
                .callerId(callSheetInfo.getCallerId())
                .answerId(callSheetInfo.getAnswerId())
                .build()
                .insert();

        // 更新话单状态
        callSheetInfo.setStatus(CallSheetStatus.正在通话.getCode());
        callSheetInfo.setStartTime(new Date());
        callSheetInfo.setRingTime(bo.getRingTime());
        callSheetInfo.setAnswerBranch(Integer.valueOf(ServletUtils.getRequest().getHeader("branch")));
        apiCallSheetInfoMapper.updateById(callSheetInfo);

        StartAnswerVo vo = new StartAnswerVo();
        if (!StartCallType.getByCode(callSheetInfo.getType())
                .getCode()
                .equals(StartCallType.客服视频.getCode())) {
            // 客服电话 不需要计费
            // 主叫计费-初始化计费
            StartBillBo callBill = new StartBillBo();
            callBill.setBillUid(callSheetInfo.getCallerId());
            callBill.setUcid(callSheetInfo.getUcid());
            startBill(callBill);
            // 被叫计费-激活第一次计费
            StartBillBo answerBill = new StartBillBo();
            answerBill.setUcid(callSheetInfo.getUcid());
            answerBill.setBillUid(callSheetInfo.getAnswerId());
            StartBill startBill = startBill(answerBill);
            if (startBill.getRestMinute() == 0) {
                throw AppServiceException.goldNotEnoughException("余额不足以通话");
            }
            // 预付费通话时间+1
            vo.setRestMinute(startBill.getRestMinute());
        }
        // TODO 创建聊天室
        String siginAnswer =
                TencentRTCUtils.genUserSig(String.valueOf(answerUser.getPlatformId()), 60);
        String siginCaller =
                TencentRTCUtils.genUserSig(String.valueOf(callUser.getPlatformId()), 60);
        vo.setRoomId(bo.getUcid());
        vo.setUserSig(siginAnswer);
        // 创建房间
        CallRoom callRoom = new CallRoom();
        callRoom.setId(callSheetInfo.getId());
        callRoom.setUcid(callSheetInfo.getUcid());
        callRoom.setStatus(RoomStatus.房间已创建.getCode());
        callRoom.setRoomNum(vo.getRoomId());
        callRoomMapper.insert(callRoom);

        // 推送主叫通话信令
        CallOperationEnum.被叫接听
                .getApply()
                .apply(
                        CallInfo.builder()
                                .userSig(siginCaller)
                                .roomId(vo.getRoomId())
                                .fromPlatFormId(callSheetInfo.getAnswerPlatformId())
                                .toPlatFormId(ListUtil.toList(callSheetInfo.getCallPlatformId()))
                                .ucid(callSheetInfo.getUcid())
                                .price(callSheetInfo.getCallPrice())
                                .build());
        // 更新被叫的状态
        apiAppUserMapper.resetUserStatus(
                callSheetInfo.getAnswerId(),
                AppUserOnlineStatus.空闲.getCode(),
                AppUserOnlineStatus.忙碌.getCode());

        // 被叫给主叫评分
        CallScore callScore =
                CallScore.builder()
                        .build()
                        .selectOne(
                                Wrappers.lambdaQuery(CallScore.class)
                                        .eq(CallScore::getToUserId, callSheetInfo.getCallerId())
                                        .eq(CallScore::getFromUserId, callSheetInfo.getAnswerId()));
        // 被叫不可能评价,除非是定向视频,且付款人是被叫,且未评分
        vo.setNeedScore(
                callSheetInfo.getType().equals(CallTypeEnum.随机视频.getCode())
                        && LoginHelper.getUserId().equals(callSheetInfo.getAnswerId())
                        && callScore == null);
        return vo;
    }

    /**
     * 主叫确认
     *
     * @author 老张
     * @param bo
     */
    @Override
    @Lock4j(
            acquireTimeout = 10000,
            name = CacheConstants.LOACK_CALL,
            keys = {"#bo.ucid"})
    public StartConfirm startConfirm(CallProcessBo bo) {
        CallSheetInfo callSheetInfo =
                apiCallSheetInfoMapper.selectOne(
                        Wrappers.lambdaQuery(CallSheetInfo.class)
                                .eq(CallSheetInfo::getCallerId, LoginHelper.getUserId())
                                .eq(CallSheetInfo::getUcid, bo.getUcid()));

        if (callSheetInfo == null) {
            throw AppServiceException.paramException("话单异常");
        }
        if (callSheetInfo.getStatus() != CallSheetStatus.正在通话.getCode()) {
            throw AppServiceException.paramException(
                    CallSheetStatus.getByCode(callSheetInfo.getStatus()).name());
        }
        // 呼叫日志
        CallLog.builder()
                .createTime(new Date())
                .ucid(callSheetInfo.getUcid())
                .type(CallSheetStatus.正在通话.getCode())
                .remark(CallSheetStatus.正在通话.name() + ":主叫确认")
                .belongUid(LoginHelper.getUserId())
                .callerId(callSheetInfo.getCallerId())
                .answerId(callSheetInfo.getAnswerId())
                .build()
                .insert();
        // 推送通话信令
        CallOperationEnum.主叫确认
                .getApply()
                .apply(
                        CallInfo.builder()
                                .fromPlatFormId(callSheetInfo.getAnswerPlatformId())
                                .toPlatFormId(ListUtil.toList(callSheetInfo.getCallPlatformId()))
                                .ucid(callSheetInfo.getUcid())
                                .price(callSheetInfo.getCallPrice())
                                .build());
        CallRoom callRoom =
                callRoomMapper.selectOne(
                        Wrappers.lambdaQuery(CallRoom.class)
                                .eq(CallRoom::getRoomNum, callSheetInfo.getUcid()));
        // 开始录音
        String recordTask = TencentRTCUtils.startRecording(callRoom.getRoomNum());
        String recordUrl = TencentRTCUtils.getRecordUrl(callRoom.getRoomNum(), recordTask);
        // 开始录音任务检测
        String requestId =
                TencentRTCUtils.startCheckVoice(
                        sysParamService.selectConfigByKey(SysConfigConsts.录音储存基础路径)
                                + callRoom.getRecordUrl(),
                        callRoom.getRoomNum());
        if (StrUtil.isNotEmpty(requestId)) {
            CallRoom newRoom = new CallRoom();
            newRoom.setId(callRoom.getId());
            newRoom.setRecordRequestId(requestId);
            callRoomMapper.updateById(newRoom);
        }

        int update =
                callRoomMapper.update(
                        Wrappers.lambdaUpdate(CallRoom.class)
                                .eq(CallRoom::getRoomNum, callSheetInfo.getUcid())
                                // .eq(CallRoom::getStatus, RoomStatus.房间已创建.getCode())
                                .set(CallRoom::getRecordTaskId, recordTask)
                                .set(CallRoom::getRecordUrl, recordUrl)
                                .set(CallRoom::getStatus, RoomStatus.进行中.getCode()));
        // 更新主叫的状态
        apiAppUserMapper.resetUserStatus(
                callSheetInfo.getCallerId(),
                AppUserOnlineStatus.空闲.getCode(),
                AppUserOnlineStatus.忙碌.getCode());

        StartConfirm vo = StartConfirm.builder().build();
        // 主叫给被叫评分
        CallScore callScore =
                CallScore.builder()
                        .build()
                        .selectOne(
                                Wrappers.lambdaQuery(CallScore.class)
                                        .eq(CallScore::getToUserId, callSheetInfo.getAnswerId())
                                        .eq(CallScore::getFromUserId, callSheetInfo.getCallerId()));
        vo.setNeedScore(callScore == null);
        return vo;
    }

    /**
     * 电话挂断
     *
     * @author 老张
     * @param bo
     */
    @Override
    @Lock4j(
            acquireTimeout = 10000,
        name = CacheConstants.LOACK_CALL,
            keys = {"#bo.ucid"})
    public StartCancel startCancel(StartCancelBo bo) {
        if (bo.getBillUid() == null) {
            throw new ServiceException("挂断人不能为空");
        }
        CallSheetInfo callSheetInfo =
                apiCallSheetInfoMapper.selectOne(
                        Wrappers.lambdaQuery(CallSheetInfo.class)
                                .eq(CallSheetInfo::getUcid, bo.getUcid())
                        // fansonคิดถึง-我就是这个意思。客户端调 answer 接口，你要根据当前话单的状态返回不同的提示信息，不要统一的“话单异常”
                        );
        if (callSheetInfo == null) {
            throw AppServiceException.paramException("话单状态异常");
        }
        //是否修改话单振铃时间（避免未接通挂断时话单振铃时长为0的问题）
        boolean updateRingTime = false;
        if (callSheetInfo.getStatus().equals(CallSheetStatus.正在通话.getCode())) {
            boolean answerCancel =
                    CallSheetStatus.isAnswerCancel(CallSheetStatus.getByCode(bo.getCancelType()));
            if (!answerCancel) {
                throw AppServiceException.paramException("正在通话的话单不允许执行未接通挂断");
            }
        } else if (callSheetInfo.getStatus().equals(CallSheetStatus.等待接通.getCode())) {
            boolean callCancel =
                    CallSheetStatus.isCallCancel(CallSheetStatus.getByCode(bo.getCancelType()));
            updateRingTime = true;
            if (!callCancel) {
                throw AppServiceException.paramException("等待接通的话单不允许执行接通挂断");
            }
        } else {
            throw AppServiceException.paramException(
                    CallSheetStatus.getByCode(callSheetInfo.getStatus()).name());
        }
        Long callerId = callSheetInfo.getCallerId();
        Long answerId = callSheetInfo.getAnswerId();
        CallSheetStatus status = CallSheetStatus.getByCode(bo.getCancelType());
        if (!CallSheetStatus.isAnswerCancel(status) && !CallSheetStatus.isCallCancel(status)) {
            throw AppServiceException.paramException("挂断状态异常");
        }
        CallLog callLog =
                CallLog.builder()
                        .createTime(new Date())
                        .ucid(callSheetInfo.getUcid())
                        .callerId(callerId)
                        .answerId(answerId)
                        .belongUid(bo.getBillUid())
                        .build();
        callSheetInfo.setStatus(status.getCode());
        callSheetInfo.setRemark(bo.getRemark());
        // 呼叫日志
        callLog.setType(status.getCode());
        callLog.setRemark(status.name());
        callLog.insert();

        Long duration = 0L;
        // 是否是已接通挂断-结算
        if (CallSheetStatus.isAnswerCancel(status)) {
            // 结算
            duration = cancelSettle(callSheetInfo, status);
        } else {
            // 通知挂断
            notifyNoAnswer(status, callSheetInfo);
        }
        //判断是否为未接通挂断，条件成立则修改话单的振铃时间
        if(updateRingTime){
            //获取振铃的开始时间（等待接通日志的创建时间则为振铃的开始时间）
            callLog = CallLog.builder().build().selectOne(
                Wrappers.lambdaQuery(CallLog.class)
                    //话单唯一标识
                    .eq(CallLog::getUcid, callSheetInfo.getUcid())
                    //等待接通的状态
                    .eq(CallLog::getType, CallSheetStatus.等待接通.getCode()));
            //通过日志的创建时间和当前时间计算相差秒数
            int ringTime = DateUtils.differentSecond(callLog.getCreateTime(), DateUtils.getNowDate());
            callSheetInfo.setRingTime(ringTime);
        }
        // 更新通话时长/支付总数/话单状态
        apiCallSheetInfoMapper.updateById(callSheetInfo);

        // 重置用户状态,客户端经常出现用户状态混乱,所以不管是在什么情况下调用了了cacelCall都要重置状态,
        // 服务端有补偿机制,客户端在无故不调用cancel的情况下,会自动调用,用户在通话状态会处于忙碌状态,掉线以后,客户端断链,不会改成离线.
        apiAppUserMapper.resetUserStatus(
                callSheetInfo.getAnswerId(),
                AppUserOnlineStatus.忙碌.getCode(),
                AppUserOnlineStatus.空闲.getCode());
        apiAppUserMapper.resetUserStatus(
                callSheetInfo.getCallerId(),
                AppUserOnlineStatus.忙碌.getCode(),
                AppUserOnlineStatus.空闲.getCode());
        return StartCancel.builder().duration(duration).build();
    }

    /**
     * 主叫给被叫打分
     *
     * @author 老张
     * @param bo
     */
    @Override
    @Lock4j(
            acquireTimeout = 10000,
            name = CacheConstants.LOACK_CALL,
            keys = {"#bo.ucid"})
    public void startScore(StartScoreBo bo) {
        CallSheetInfo callSheetInfo =
                apiCallSheetInfoMapper.selectOne(
                        Wrappers.lambdaQuery(CallSheetInfo.class)
                                .eq(CallSheetInfo::getPayUid, LoginHelper.getUserId())
                                .eq(CallSheetInfo::getScoreFlag, EnableEnum.NO.getCode())
                                .eq(CallSheetInfo::getUcid, bo.getUcid())
                                .in(
                                        CallSheetInfo::getStatus,
                                        CallSheetStatus.已接通主叫挂断.getCode(),
                                        CallSheetStatus.已接通被叫挂断.getCode(),
                                        CallSheetStatus.已接通系统挂断.getCode()));
        if (callSheetInfo == null) {
            throw AppServiceException.paramException("当前话单暂时无法评分");
        }

        if (StartCallType.getByCode(callSheetInfo.getType())
                .getCode()
                .equals(StartCallType.客服视频.getCode())) {
            // 客服给用户打电话不用上传心跳和质检
            // 勿-你不处理就行了呗
            return;
        }

        try {
            CallScore.builder()
                    .fromUserId(callSheetInfo.getPayUid())
                    .toUserId(callSheetInfo.getBenefitUid())
                    .build()
                    .insert();
        } catch (Exception e) {
            throw AppServiceException.paramException("您已经评过分了哦~");
        }
        Long payUid = callSheetInfo.getPayUid();
        if (!payUid.equals(LoginHelper.getUserId())) {
            throw AppServiceException.paramException("您当前无法评分哦~");
        }
        // -1 表示放弃评分
        if (bo.getScore() >= 0) {
            // monster  应该都是男的评分是吧 对呀
            AppUserExt appUserExt = appUserExtMapper.selectById(callSheetInfo.getBenefitUid());
            BigDecimal callScore = appUserExt.getCallScore();
            BigDecimal callCount = appUserExt.getCallCount();
            // 最新分数
            BigDecimal divide =
                    // 总次数
                    callCount
                            // X平均分=总分
                            .multiply(callScore)
                            // 总分+当前得分=新总分
                            .add(BigDecimal.valueOf(bo.getScore()))
                            // 新总分/(总次数+1)=新平均分,四舍五入
                            .divide(callCount.add(BigDecimal.valueOf(1)), 2, RoundingMode.HALF_UP);

            appUserExt.setCallCount(callCount.add(BigDecimal.valueOf(1)));
            appUserExt.setCallScore(divide);
            appUserExtMapper.updateById(appUserExt);
            callSheetInfo.setScoreFlag(MsgConstant.YES);
            apiCallSheetInfoMapper.updateById(callSheetInfo);
        }

        String content = "";
        if (bo.getScore() == -1) {
            content = "对方放弃了评分";
        }
        if (bo.getScore() >= 1 && bo.getScore() <= 2) {
            content = "对方对本次通话给出一般的评分,加油哟~";
        }
        if (bo.getScore() == 3) {
            content = "对方对本次通话给出很满意的评价~";
        }
        if (bo.getScore() > 3) {
            content = "对方对本次通话给出非常满意的评价~";
        }
        AppUser appUser = apiAppUserMapper.selectById(callSheetInfo.getBenefitUid());
        PushMessage.sendTextSingleSysScore(appUser.getPlatformId(), content);
    }

    /**
     * 通话计费功能的实现. 当调用两次计费接口时开始记一次费用. 如果缺少一次调用, 则需要解散房间.
     *
     * @param bo 业务对象, 包含计费所需的参数.
     * @return 返回计费结果对象.
     * @throws ServiceException 如果计费人为空, 则抛出服务异常.
     */
    @Override
    @Lock4j(
            acquireTimeout = 10000,
            name = CacheConstants.LOACK_CALL,
            keys = {"#bo.ucid"})
    public StartBill startBill(StartBillBo bo) {
        // 检查计费人是否为空
        if (bo.getBillUid() == null) {
            throw new ServiceException("计费人不能为空");
        }

        // 查询符合条件的通话记录
        CallSheetInfo callSheetInfo =
                apiCallSheetInfoMapper.selectOne(
                        Wrappers.lambdaQuery(CallSheetInfo.class)
                                .eq(CallSheetInfo::getUcid, bo.getUcid())
                                .and(
                                        item ->
                                                item.eq(CallSheetInfo::getCallerId, bo.getBillUid())
                                                        .or()
                                                        .eq(
                                                                CallSheetInfo::getAnswerId,
                                                                bo.getBillUid())));

        // 如果没有找到符合条件的通话记录, 抛出参数异常
        if (callSheetInfo == null) {
            throw AppServiceException.paramException("话单状态异常");
        }

        // 判断通话类型, 如果是客服视频通话, 不需要上传心跳和质检
        if (StartCallType.getByCode(callSheetInfo.getType())
                .getCode()
                .equals(StartCallType.客服视频.getCode())) {
            return StartBill.builder().build();
        }

        // 检查通话记录的状态是否为"正在通话", 否则抛出参数异常
        if (!callSheetInfo.getStatus().equals(CallSheetStatus.正在通话.getCode())) {
            throw AppServiceException.paramException(
                    CallSheetStatus.getByCode(callSheetInfo.getStatus()).name());
        }

        // 调用Lua脚本计算心跳次数和计费次数
        CallCountUtils.CallInfo callInfo = CallCountUtils.callCaount(bo.getUcid());
        // 更新通话记录的心跳次数
        callSheetInfo.setKeepLiveTimes(callInfo.getKeepLiveTimes());

        // 创建一个新的通话记录对象用于更新数据库
        CallSheetInfo newC = new CallSheetInfo();
        newC.setId(callSheetInfo.getId());
        newC.setKeepLiveTimes(callInfo.getKeepLiveTimes());

        // 计算剩余可用通话分钟数
        Integer restMinute = 0;
        if (callInfo.getFreeFlag()) {
            CallFreeBo callFreeBo = callIsFree(callSheetInfo.getPayUid());
            if (callFreeBo.getIsFree()) {
                restMinute = toFree(callSheetInfo, callInfo, newC, callFreeBo);
            } else {
                restMinute = toFees(callSheetInfo, callInfo, newC);
            }
        } else {
            // 第一次不收费，默认可以成功收费。+1是为了挂断由第二次挂。
            restMinute = getRestMinute(callSheetInfo.getPayUid(), callSheetInfo.getCallPrice()) + 1;
        }

        // 创建并插入呼叫日志
        CallLog.builder()
                .createTime(new Date())
                .ucid(callSheetInfo.getUcid())
                .type(CallSheetStatus.正在通话.getCode())
                .remark(CallSheetStatus.正在通话.name() + ":keepLive:" + callInfo.getKeepLiveTimes())
                .belongUid(bo.getBillUid())
                .callerId(callSheetInfo.getCallerId())
                .answerId(callSheetInfo.getAnswerId())
                .build()
                .insert();

        // 更新通话记录
        apiCallSheetInfoMapper.updateById(newC);

        // 如果心跳之前用户是空闲状态, 则将其状态改为忙碌
        apiAppUserMapper.resetUserStatus(
                LoginHelper.getUserId(),
                AppUserOnlineStatus.空闲.getCode(),
                AppUserOnlineStatus.忙碌.getCode());

        // 返回计费结果
        return StartBill.builder().restMinute(restMinute).build();
    }

    /**
     * 处理通话的免费时长扣除逻辑
     *
     * <p>此方法用于处理通话中的免费时长扣除逻辑，它将更新通话记录中的免费通话次数，并计算剩余的免费通话时长
     *
     * @param callSheetInfo 呼叫表格信息，包含免费次数等信息
     * @param callInfo 呼叫信息，包含价格倍数等
     * @param newC 新的呼叫表格信息对象，用于存储更新后的信息
     * @param callFreeBo 呼叫免费业务对象，包含剩余免费分钟数
     * @return 返回更新后的剩余分钟数
     * @author 老张
     */
    private Integer toFree(
            CallSheetInfo callSheetInfo,
            CallCountUtils.CallInfo callInfo,
            CallSheetInfo newC,
            CallFreeBo callFreeBo) {
        // 设置新的呼叫记录中的价格倍数
        newC.setPriceTimes(callInfo.getPriceTimes());
        // 更新免费通话次数
        newC.setFreeTimes(callSheetInfo.getFreeTimes() + 1);
        // 计算剩余的免费通话时长
        return callFreeBo.getRestM()
                + getRestMinute(callSheetInfo.getPayUid(), callSheetInfo.getCallPrice())
                + 1;
    }

    /**
     * 将通话状态从免费转换为收费
     *
     * <p>此方法负责在通话结束后处理收费逻辑。它会检查用户的余额， 如果余额充足，则扣除通话费用，并更新用户的通话记录； 如果余额不足，则设置一个标记，让系统在15秒后自动挂断电话。
     *
     * @param callSheetInfo 通话详情信息，包含通话费用等数据
     * @param callInfo 通话信息工具类对象，包含计费相关的信息
     * @param newC 新的通话记录对象，用于存储更新后的通话信息
     * @return 剩余可用通话时间（分钟数）。如果扣费失败，则返回0
     * @author 老张
     */
    private Integer toFees(
            CallSheetInfo callSheetInfo, CallCountUtils.CallInfo callInfo, CallSheetInfo newC) {
        // 本次不免费
        Integer count =
                apiAppUserMapper.logSubGoldBalance(
                        PayLogDTO.builder()
                                .traceId(callSheetInfo.getId())
                                .uid(callSheetInfo.getPayUid())
                                .changeNum(callSheetInfo.getCallPrice())
                                .serviceTag(
                                        "用户拨打电话-startBill-"
                                                + callInfo.getPriceTimes()
                                                + "-"
                                                + callSheetInfo.getUcid())
                                .logType(PayLogTypeEnums.消费_拨打视频)
                                .build());
        if (count == 1) {
            // 扣费成功，设置本次通话的计费次数和累计费用
            newC.setPriceTimes(callInfo.getPriceTimes());
            newC.setFeeTimes(callSheetInfo.getFeeTimes() + 1);
            // 更新累计支付金额
            Integer addPayCost =
                    apiCallSheetInfoMapper.addPayCost(
                            callSheetInfo.getId(), callSheetInfo.getCallPrice());
            log.info("更新累计支付：{}", addPayCost);
            // 返回扣费成功后的剩余可用通话时间
            return getRestMinute(callSheetInfo.getPayUid(), callSheetInfo.getCallPrice()) + 1;
        } else {
            // 扣费失败，设置15秒后自动挂断的缓存标记
            RedisUtils.setCacheObject(
                    CacheConstants.CALL_BILL_UPDATE + callSheetInfo.getUcid(),
                    "1",
                    Duration.ofSeconds(15));
            // 返回扣费失败的结果
            return 0;
        }
    }

    /**
     * 是否通话免费 Monsters(sudo rm -rf) 男号vip每日免费通话2分钟
     *
     * @author 老张
     */
    private CallFreeBo callIsFree(Long payUid) {
        AppUser appUser = apiAppUserMapper.selectById(payUid);
        if (!appUser.getSex().equals(SexEnum.男.getCode())) {
            return CallFreeBo.builder().isFree(false).restM(0).build();
        }
        AppUserVip appUserVip = appUserVipMapper.selectById(payUid);
        Boolean isVip =
                appUserVip.getCommonVipFlag().equals(EnableEnum.YES.getCode())
                        && appUserVip.getCommonVipExpireTime().getTime() >= new Date().getTime();
        if (!isVip) {
            return CallFreeBo.builder().isFree(false).restM(0).build();
        }
        String strFreeTimes = sysParamService.selectConfigByKey(SysConfigConsts.男VIP每日免费通话时间);

        if (StrUtil.isEmpty(strFreeTimes)) {
            return CallFreeBo.builder().isFree(false).restM(0).build();
        }
        Integer times = Integer.parseInt(strFreeTimes);

        // 每日次数且匹配成功才算
        long between = DateUtil.between(new Date(), DateUtil.endOfDay(new Date()), DateUnit.SECOND);
        Boolean isFree =
                RedisUtils.setAtomicValueAdd(
                        CacheConstants.LIMIT_MALE_FREE_CALL_DAY + payUid,
                        times,
                        Integer.parseInt(String.valueOf(between)));
        if (isFree) {
            // 已经使用的分钟数
            Object cacheObject =
                    RedisUtils.getCacheObject(CacheConstants.LIMIT_MALE_FREE_CALL_DAY + payUid);
            // 已经使用的分钟数
            Integer useM = (Integer) cacheObject;
            // 剩余的分钟数
            Integer restM = useM == null ? times : times - useM;
            log.info(
                "男号每日免费分钟数,{},已经免费分钟数,{},剩余免费分钟数:{},付费人ID:{}",
                times,
                cacheObject,
                restM, appUser.getPlatformId()); //   return !isFree;
            return CallFreeBo.builder()
                .isFree(cacheObject == null || restM < times)
                .restM(restM + 1)
                .build();
        } else {
            return CallFreeBo.builder().isFree(isFree).restM(0).build();
        }
    }

    /**
     * 通话质检
     *
     * @author 老张
     * @param bo
     */
    @Override
    public CheckVo startCheck(StartCheck bo) {
        Long userId = LoginHelper.getUserId();
        CallSheetInfo callSheetInfo =
                apiCallSheetInfoMapper.selectOne(
                        Wrappers.lambdaQuery(CallSheetInfo.class)
                                //  .eq(CallSheetInfo::getStatus, CallSheetStatus.正在通话.getCode())
                                .eq(CallSheetInfo::getUcid, bo.getUcid())
                                .and(
                                        item ->
                                                item.eq(CallSheetInfo::getCallerId,userId)
                                                        .or()
                                                        .eq(CallSheetInfo::getAnswerId,userId)
                                )
                );
        if (callSheetInfo == null) {
            throw AppServiceException.paramException("话单状态异常");
        }
        if (StartCallType.getByCode(callSheetInfo.getType())
                .getCode()
                .equals(StartCallType.客服视频.getCode())) {
            // 客服给用户打电话不用上传心跳和质检
            // 勿-你不处理就行了呗
            return new CheckVo(false);
        }

        if (!callSheetInfo.getStatus().equals(CallSheetStatus.正在通话.getCode())) {
            throw AppServiceException.paramException(
                    CallSheetStatus.getByCode(callSheetInfo.getStatus()).name());
        }
        if (userId.equals(callSheetInfo.getPayUid())) {
            // pay 是男号 benefit 是女号 ,男号不检测
            return new CheckVo(false);
        }

        boolean flag = TencentRTCUtils.startCheckVideoSnap(bo.getPicUrl());
        String snapKey = CacheConstants.CALL_SNAP_TIMES_KEY + callSheetInfo.getUcid();
        if (flag) {
            // 保存用户违规截图
            CallCheck.builder()
                .ucid(callSheetInfo.getUcid())
                .userId(userId)
                .platformId(LoginHelper.getPlatformId())
                .picUrl(bo.getPicUrl())
                .createTime(new Date()).build().insert();
            // 从redis中获取违规次数，如果为空，则表示第一次违规，不做任何操作。
            String times = RedisUtils.getCacheObject(snapKey);
            if (StringUtils.isEmpty(times)) {
                // 记录当前违规。
                RedisUtils.setCacheObject(snapKey, "1");
                return new CheckVo(true);
            } else {
                // 有舍有得-    封10分钟第一次，第二次封号1小时，第三次及以后封号3小时扣3000砖石
                // fansonคิดถึง- 爱洞涉黄封号就十分钟就行了，别搞那么复杂
                AppUser user = apiAppUserMapper.selectById(userId);
                Long anotherPlatformId;
                if(userId.equals(callSheetInfo.getCallerId())){
                    anotherPlatformId = callSheetInfo.getAnswerPlatformId();
                } else {
                    anotherPlatformId = callSheetInfo.getCallPlatformId();
                }
                appUserAuthService.banUser(user, 10, "通话涉黄");

                //踢下线违规用户
                PushMessage.sendSysNotify(user.getPlatformId(), "因您通话违规，本次通话已被系统自动挂断。并且您的账号将被封禁十分钟，请十分钟后重新登录。");
                PushMessage.kickout(user.getPlatformId(), "因您通话违规，本次通话已被系统自动挂断。并且您的账号将被封禁十分钟，请十分钟后重新登录。");

                //通知对方
                PushMessage.sendSysNotify(anotherPlatformId, "因对方违规，本次通话已被系统自动挂断，并且对方已被封号。");

                StartCancelBo cancelBo = new StartCancelBo();
                cancelBo.setCancelType(CallSheetStatus.已接通系统挂断.getCode());
                cancelBo.setRemark("通话涉黄");
                cancelBo.setUcid(callSheetInfo.getUcid());
                cancelBo.setBillUid(SysIdConstant.系统);
                callService.startCancel(cancelBo);
                // 删除当前话单的违规标识。
                RedisUtils.deleteObject(snapKey);
            }
        }
        return new CheckVo(flag);
    }

    /**
     * 通话回调-进入房间
     *
     * @author 老张
     */
    @Override
    public void callbackEnter(RtcRoomCallBackBo rtcRoomCallBackBo) {
        int update =
                callRoomMapper.update(
                        Wrappers.lambdaUpdate(CallRoom.class)
                                .eq(CallRoom::getStatus, RoomStatus.房间已创建.getCode())
                                .eq(
                                        CallRoom::getRoomNum,
                                        rtcRoomCallBackBo.getEventInfo().getRoomId())
                                .set(CallRoom::getStatus, RoomStatus.进行中.getCode()));
    }

    /**
     * 通话回调-离开房间
     *
     * @author 老张
     */
    @Override
    public void callbackLeave(RtcRoomCallBackBo rtcRoomCallBackBo) {
        CallRoom callRoom =
                callRoomMapper.selectOne(
                        Wrappers.lambdaQuery(CallRoom.class)
                                .eq(CallRoom::getStatus, RoomStatus.进行中.getCode())
                                .eq(
                                        CallRoom::getRoomNum,
                                        rtcRoomCallBackBo.getEventInfo().getRoomId()));
        if (callRoom != null) {
            StartCancelBo cancelBo = new StartCancelBo();
            cancelBo.setCancelType(CallSheetStatus.已接通系统挂断.getCode());
            cancelBo.setUcid(callRoom.getUcid());
            cancelBo.setBillUid(SysIdConstant.系统);
            cancelBo.setRemark("通话回调-离开房间");
            callService.startCancel(cancelBo);
        }
    }

    /**
     * 通话断链重新加入
     *
     * @author 老张
     */
    @Override
    @Lock4j(
            acquireTimeout = 10000,
            name = CacheConstants.LOACK_CALL,
            keys = {"#bo.ucid"})
    public StartJoinVo startJoin(StartJoinBo bo) {
        CallSheetInfo callSheetInfo =
                apiCallSheetInfoMapper.selectOne(
                        Wrappers.lambdaQuery(CallSheetInfo.class)
                                .eq(CallSheetInfo::getStatus, CallSheetStatus.正在通话.getCode())
                                .eq(CallSheetInfo::getUcid, bo.getUcid())
                                .and(
                                        item ->
                                                item.eq(
                                                                CallSheetInfo::getAnswerId,
                                                                LoginHelper.getUserId())
                                                        .or()
                                                        .eq(
                                                                CallSheetInfo::getCallerId,
                                                                LoginHelper.getUserId())),
                        false);
        if (callSheetInfo == null) {
            throw AppServiceException.paramException("暂无可加入聊天");
        }
        CallRoom callRoom =
                callRoomMapper.selectOne(
                        Wrappers.lambdaQuery(CallRoom.class)
                                .eq(CallRoom::getStatus, RoomStatus.进行中.getCode())
                                .eq(CallRoom::getRoomNum, bo.getUcid()));
        if (callRoom == null) {
            throw AppServiceException.paramException("暂无可加入的房间");
        }

        // 加入之前如果是空闲，则改为忙碌
        apiAppUserMapper.resetUserStatus(
                LoginHelper.getUserId(),
                AppUserOnlineStatus.空闲.getCode(),
                AppUserOnlineStatus.忙碌.getCode());

        String siginAnswer =
                TencentRTCUtils.genUserSig(String.valueOf(LoginHelper.getPlatformId()), 60);
        StartJoinVo startJoinVo = new StartJoinVo();
        startJoinVo.setUserSig(siginAnswer);
        startJoinVo.setRestMinute(
                getRestMinute(callSheetInfo.getPayUid(), callSheetInfo.getCallPrice()));
        startJoinVo.setRoomId(callSheetInfo.getUcid());

        startJoinVo.setCallTime(
                DateUtil.between(new Date(), callSheetInfo.getStartTime(), DateUnit.SECOND));
        return startJoinVo;
    }

    /**
     * 检查是否有正在通话的话单
     *
     * @author 老张
     */
    @Override
    public QueryCallSheetVo queryCallSheet() {
        CallSheetInfo callSheetInfo =
                apiCallSheetInfoMapper.selectOne(
                        Wrappers.lambdaQuery(CallSheetInfo.class)
                                .eq(CallSheetInfo::getStatus, CallSheetStatus.正在通话.getCode())
                                .and(
                                        item ->
                                                item.eq(
                                                                CallSheetInfo::getAnswerId,
                                                                LoginHelper.getUserId())
                                                        .or()
                                                        .eq(
                                                                CallSheetInfo::getCallerId,
                                                                LoginHelper.getUserId())),
                        false);
        QueryCallSheetVo callSheetVo = QueryCallSheetVo.builder().build();
        if (callSheetInfo != null) {
            callSheetVo = BeanUtil.toBean(callSheetInfo, QueryCallSheetVo.class);

            // 查询对方的用户信息
            Long baseUid =
                    callSheetInfo.getCallerId().equals(LoginHelper.getUserId())
                            ? LoginHelper.getUserId()
                            : callSheetInfo.getAnswerId();
            Long basePlatformId =
                    callSheetInfo.getCallerId().equals(LoginHelper.getUserId())
                            ? callSheetInfo.getCallPlatformId()
                            : callSheetInfo.getAnswerPlatformId();

            AppUserExt appUserExt = appUserExtMapper.selectById(baseUid);
            callSheetVo.setId(baseUid);
            callSheetVo.setPlatformId(basePlatformId);
            callSheetVo.setNickName(appUserExt.getNickName());
            callSheetVo.setHeadUrl(appUserExt.getHeadUrl());

            callSheetVo.setUcid(callSheetInfo.getUcid());
            callSheetVo.setRestMinute(
                    getRestMinute(callSheetInfo.getPayUid(), callSheetInfo.getCallPrice()));
            return callSheetVo;
        }
        // 上线推送
        SpringUtil.publishEvent(PushOnlineEvent.builder().uid(LoginHelper.getUserId()).build());
        throw AppServiceException.paramException("没有正在通话的话单");
    }

    @Override
    public QueryUserVo queryUser(BaseId baseId) {
        AppUser appUser =
                apiAppUserMapper.selectOne(
                        Wrappers.lambdaQuery(AppUser.class)
                                .select(AppUser::getPlatformId)
                                .eq(AppUser::getPlatformId, baseId.getId()));
        if (appUser != null) {
            AppUserExt appUserExt = appUserExtMapper.selectById(appUser.getId());
            QueryUserVo vo =
                    QueryUserVo.builder()
                            .sex(appUser.getSex())
                            .nickName(appUserExt.getNickName())
                            .headUrl(appUserExt.getHeadUrl())
                            .id(appUser.getId())
                            .platformId(baseId.getId())
                            .build();
            return vo;
        }
        throw AppServiceException.paramException("用户未找到");
    }

    /**
     * 开始录音
     *
     * @author 老张
     */
    @Override
    public void startRecord(RtcRecordCallBackBo bo) {
        if (RtcRecordCallBackBo.EventType.M3U8生成.getCode().equals(bo.getEventType())) {
            log.info("M3U8生成回调启动:{}", bo.getEventInfo().getRoomId());
            RtcRecordCallBackBo.EventInfo eventInfo = bo.getEventInfo();
            String roomId = eventInfo.getRoomId();
            CallRoom callRoom =
                    callRoomMapper.selectOne(
                            Wrappers.lambdaQuery(CallRoom.class).eq(CallRoom::getRoomNum, roomId));
            if (callRoom != null) {
                // 开始录音任务
                //                String requestId =
                //                        TencentRTCUtils.startCheckVoice(
                //
                // sysParamService.selectConfigByKey(SysConfigConsts.录音储存基础路径)
                //                                        + callRoom.getRecordUrl(),
                //                                callRoom.getRoomNum());
                //                if (StrUtil.isNotEmpty(requestId)) {
                //                    CallRoom newRoom = new CallRoom();
                //                    newRoom.setId(callRoom.getId());
                //                    newRoom.setRecordRequestId(requestId);
                //                    callRoomMapper.updateById(newRoom);
                //                }
            }
        }
        if (RtcRecordCallBackBo.EventType.云端录制模块退出.getCode().equals(bo.getEventType())) {
            log.info("云端录制模块退出回调启动:{}", bo.getEventInfo().getRoomId());
            StartCancelBo cancelBo = new StartCancelBo();
            cancelBo.setCancelType(CallSheetStatus.已接通系统挂断.getCode());
            cancelBo.setUcid(bo.getEventInfo().getRoomId());
            cancelBo.setRemark("云端录制模块退出");
            cancelBo.setBillUid(SysIdConstant.系统);
            callService.startCancel(cancelBo);
        }
    }

    /**
     * 语音检测
     *
     * @author 老张
     */
    @Override
    public void rtcContentCheck(RtcContentCheckBo res) {
        if (res.getRtn() == 0) {
            String audioId = res.getAudioId();
            CallSheetInfo callSheetInfo =
                    apiCallSheetInfoMapper.selectOne(
                            Wrappers.lambdaQuery(CallSheetInfo.class)
                                    .eq(CallSheetInfo::getUcid, audioId));
            if (!"pass".equals(res.getResult())) {
                if (callSheetInfo != null) {
                    CallVoiceCheck callVoiceCheck = new CallVoiceCheck();
                    callVoiceCheck.setUcid(audioId);
                    callVoiceCheck.setAnswerPlatformId(callSheetInfo.getAnswerPlatformId());
                    callVoiceCheck.setCallPlatformId(callSheetInfo.getCallPlatformId());
                    callVoiceCheck.setKeyWord(
                            BeanUtil.copyToList(res.getDetails(), CallVoiceCheck.Details.class));
                    callVoiceCheck.setVoiceUrl(res.getPartAudioUrl());
                    callVoiceCheck.setVoiceText(res.getPartAudioText());
                    // 疑似违规
                    callVoiceCheckMapper.insert(callVoiceCheck);
                }
            }
            if (callSheetInfo != null
                    && !CallSheetStatus.正在通话.getCode().equals(callSheetInfo.getStatus())) {
                CallRoom callRoom =
                        callRoomMapper.selectOne(
                                Wrappers.lambdaQuery(CallRoom.class)
                                        .eq(CallRoom::getRoomNum, res.getAudioId()));
                // 停止录音检测
                TencentRTCUtils.stopCheckVoice(callRoom.getRecordRequestId());
            }
        } else {
            if (res.getRtn() != 4306) {
                // @FIXME
                // -请问4306是什么异常，文档没有写
                // -4306后马上就是0
                // -4306切片的时候空针，咱们可以不用管
                // -空帧
                log.error("语音检测失败..{}", res.getMessage());
                // 停止录音检测
                TencentRTCUtils.stopCheckVoice(res.getRequestId());
            }
        }
    }

    /**
     * 执行挂断后的结算流程
     *
     * @param callSheetInfo 呼叫单信息
     * @param status 呼叫状态
     * @return 通话时长
     */
    private Long cancelSettle(CallSheetInfo callSheetInfo, CallSheetStatus status) {
        // 设置通话结束时间
        Date date = new Date();
        callSheetInfo.setStopTime(date);
        // 计算通话时长（秒）
        long between = DateUtil.between(callSheetInfo.getStartTime(), date, DateUnit.SECOND);
        callSheetInfo.setDuration(between);

        // 客服视频通话不收费
        Long duration = 0L;
        if (!StartCallType.getByCode(callSheetInfo.getType())
                .getCode()
                .equals(StartCallType.客服视频.getCode())) {
            // 更新用户等级（根据通话时长等信息调整用户等级）
            updateLevel(callSheetInfo);
            // 检查并返回预付费用户的话费余额
            duration = checkReturnMoney(callSheetInfo);
        }

        // 清除Redis中与计费相关的数据
        RedisUtils.deleteObject(CacheConstants.CALL_BILL_UPDATE + callSheetInfo.getUcid());
        RedisUtils.deleteObject(
                CacheConstants.CALL_KEEP_LIVE_TIMES_UPDATE + callSheetInfo.getUcid());
        RedisUtils.deleteObject(CacheConstants.CALL_PRICE_TIMES_UPDATE + callSheetInfo.getUcid());

        // 更新并解散通话房间
        CallRoom callRoom =
                callRoomMapper.selectOne(
                        Wrappers.lambdaQuery(CallRoom.class)
                                .eq(CallRoom::getUcid, callSheetInfo.getUcid()));
        if (callRoom != null) {
            callRoom.setEndTime(new Date());
            callRoom.setStatus(RoomStatus.房间已解散.getCode());
            callRoomMapper.updateById(callRoom);

            // 解散房间
            TencentRTCUtils.dismissRoom(callRoom.getRoomNum());
            // 停止录音
            TencentRTCUtils.stopRecording(callRoom.getRecordTaskId());
            // 停止录音检测
            TencentRTCUtils.stopCheckVoice(callRoom.getRecordRequestId());
        }

        // 准备平台通知信息
        List<Long> toPlatFormIds = new ArrayList<>();
        Long fromPlatFormId = null;
        if (status.getCode().equals(CallSheetStatus.已接通系统挂断.getCode())) {
            fromPlatFormId = SysIdConstant.系统;
            toPlatFormIds =
                    ListUtil.toList(
                            callSheetInfo.getAnswerPlatformId(), callSheetInfo.getCallPlatformId());
        } else {
            if (status.getCode().equals(CallSheetStatus.已接通被叫挂断.getCode())) {
                fromPlatFormId = callSheetInfo.getAnswerPlatformId();
                toPlatFormIds = ListUtil.toList(callSheetInfo.getCallPlatformId());
            } else if (status.getCode().equals(CallSheetStatus.已接通主叫挂断.getCode())) {
                fromPlatFormId = callSheetInfo.getCallPlatformId();
                toPlatFormIds = ListUtil.toList(callSheetInfo.getAnswerPlatformId());
            }
        }

        // 执行挂断通话操作
        CallOperationEnum.挂断通话
                .getApply()
                .apply(
                        CallInfo.builder()
                                .duration(duration)
                                .fromPlatFormId(fromPlatFormId)
                                .toPlatFormId(toPlatFormIds)
                                .ucid(callSheetInfo.getUcid())
                                .price(callSheetInfo.getCallPrice())
                                .build());
        return duration;
    }

    /**
     * 检查退费,预扣费
     *
     * @author 老张
     */
    private Long checkReturnMoney(CallSheetInfo callSheetInfo) {

        // 计费次数,包含 免费+收费
        BigDecimal priceTimes = BigDecimal.valueOf(callSheetInfo.getPriceTimes());
        // 通话时长
        Long duration = callSheetInfo.getDuration();
        // 实际应收收费次数(包含免费+收费)
        BigDecimal realPriceNum =
                BigDecimal.valueOf(duration).divide(BigDecimal.valueOf(60), 0, RoundingMode.UP);
        // 差值 = 实际应收次数 - 收费计价次数
        BigDecimal subtract = realPriceNum.subtract(priceTimes);

        // 默认情况下 收益=支出(仅有收费)
        callSheetInfo.setEarnCost(callSheetInfo.getPayCost());

        if (NumberUtil.equals(priceTimes, realPriceNum)) {
            // x:00->x:49 手动挂断
            log.info("心跳计价次数和通话时间计算次数相等,无需处理");
        }
        if (NumberUtil.isLess(priceTimes, realPriceNum)) {
            // 卡点挂断,60秒挂断,但是网络原因 61秒才挂完也会算一分钟,除非他没钱了
            // 1.系统挂断不及时,如果余额不扣,扣款不成功,原则上通话超过1秒就算一分钟
            // 2.由于各种原因心跳没上报,但是调用了挂断.系统后台自动挂断
            log.info("心跳次数小于通话时间,追加扣款");
            BigDecimal multiply = callSheetInfo.getCallPrice().multiply(subtract);

            Integer res =
                    apiAppUserMapper.logSubGoldBalance(
                            PayLogDTO.builder()
                                    .traceId(callSheetInfo.getId())
                                    .uid(callSheetInfo.getPayUid())
                                    .changeNum(multiply)
                                    .serviceTag(
                                            "心跳接收到的计费次数和通话时间不匹配,通话结算时长:"
                                                    + duration
                                                    + ",心跳上报价格次数:"
                                                    + priceTimes
                                                    + "-"
                                                    + callSheetInfo.getUcid())
                                    .logType(PayLogTypeEnums.消费_异常通话结算)
                                    .build());
            if (res == 1) {
                // 意外情况 的支出不计入收入,只计入支出
                callSheetInfo.setPayCost(callSheetInfo.getPayCost().add(multiply));
            }
        }
        if (NumberUtil.isGreater(priceTimes, realPriceNum)) {
            log.info("心跳次数大于通话时间,预付费返款");
            // 49秒以后调startBill 会扣除下一分钟的余额,49秒以后不足1分钟挂断,返1分钟的单价
            // 免费的通话时间
            Integer freeTimes = callSheetInfo.getFreeTimes();
            // 收费的通话时间
            Integer feeTimes = callSheetInfo.getFeeTimes();
            if (feeTimes > 0) {
                // 收费的时间大于0说明免费的已经用完了
                // 返话费
                apiAppUserMapper.logForAddGoldBalance(
                        PayLogDTO.builder()
                                .traceId(callSheetInfo.getId())
                                .uid(callSheetInfo.getPayUid())
                                .changeNum(callSheetInfo.getCallPrice())
                                .serviceTag("预付费返账：" + callSheetInfo.getUcid())
                                .logType(PayLogTypeEnums.收益_通话预付费返还)
                                .build());
                callSheetInfo.setPayCost(
                        callSheetInfo.getPayCost().subtract(callSheetInfo.getCallPrice()));
                // 更新收入
                callSheetInfo.setEarnCost(callSheetInfo.getPayCost());
                callSheetInfo.setFeeTimes(callSheetInfo.getFeeTimes() - 1);
            } else {
                callSheetInfo.setFreeTimes(callSheetInfo.getFreeTimes() - 1);
                // 返还一分钟免费
                RedisUtils.decrAtomicValue(
                        CacheConstants.LIMIT_MALE_FREE_CALL_DAY + callSheetInfo.getPayUid());
            }
        }
        // 邀请人返利---
        BigDecimal earn = invitedReward(callSheetInfo);
        // 受益人加钱
        apiAppUserMapper.logAddDiamondBalance(
                PayLogDTO.builder()
                        .traceId(callSheetInfo.getId())
                        .uid(callSheetInfo.getBenefitUid())
                        .changeNum(earn)
                        .serviceTag("用户接听电话-话单号:" + callSheetInfo.getUcid())
                        .logType(PayLogTypeEnums.收益_接受视频)
                        .build());
        return duration;
    }

    /**
     * 邀请返利 邀请返利用户出
     *
     * @author 老张
     */
    private BigDecimal invitedReward(CallSheetInfo callSheetInfo) {
        Long benefitUid = callSheetInfo.getBenefitUid();
        AppUser appUser = apiAppUserMapper.selectById(benefitUid);
        Long pid = appUser.getPid();
        if (pid == null) {
            return callSheetInfo.getEarnCost();
        }
        BigDecimal withDrawRate =
                new BigDecimal(sysParamService.selectConfigByKey(SysConfigConsts.邀请视频收益奖励));
        // 收益比例
        BigDecimal rate = withDrawRate.divide(BigDecimal.valueOf(100), 2, RoundingMode.DOWN);

        BigDecimal earnCost = callSheetInfo.getEarnCost();
        // 收益乡下取整
        BigDecimal earn = earnCost.multiply(rate).setScale(0, RoundingMode.DOWN);
        // 邀请人加钱
        BigDecimal earnTotal = earnCost.subtract(earn);
        callSheetInfo.setEarnCost(earnTotal);
        if (NumberUtil.isGreaterOrEqual(earnTotal, BigDecimal.ONE)) {
            // 邀请人加钱
            apiAppUserMapper.logAddDiamondBalance(
                    PayLogDTO.builder()
                            .traceId(callSheetInfo.getId())
                            .uid(pid)
                            .changeNum(earn)
                            .serviceTag("用户接听电话-邀请人收益-话单号:" + callSheetInfo.getUcid())
                            .logType(PayLogTypeEnums.收益_邀请视频收益)
                            .build());
        }
        return callSheetInfo.getEarnCost();
    }

    /**
     * 更新用户等级
     *
     * @author 老张
     */
    private void updateLevel(CallSheetInfo sheetInfo) {
        // 通话持续的分钟向上去整
        BigDecimal durationMi =
                BigDecimal.valueOf(sheetInfo.getDuration())
                        .divide(BigDecimal.valueOf(60), 0, RoundingMode.UP);
        // 通话时间总时长
        AppUserExt callExt = appUserExtMapper.selectById(sheetInfo.getCallerId());
        AppUserExt answerExt = appUserExtMapper.selectById(sheetInfo.getAnswerId());
        // 更新累计通话时长,
        appUserExtMapper.updateById(
                new AppUserExt()
                        .setId(callExt.getId())
                        .setCallDurationSum(callExt.getCallDurationSum() + durationMi.intValue()));
        appUserExtMapper.updateById(
                new AppUserExt()
                        .setId(answerExt.getId())
                        // 通话成功次数
                        .setCallSuccess(answerExt.getCallSuccess() + 1)
                        // 通话总时长
                        .setCallDurationSum(
                                answerExt.getCallDurationSum() + durationMi.intValue()));
        // 查询所有的等级规则，降序
        List<UserLevel> userLevels =
                userLevelMapper.selectList(
                        Wrappers.lambdaQuery(UserLevel.class).orderByDesc(UserLevel::getLevel));
        updateLevel(userLevels, callExt, durationMi);
        updateLevel(userLevels, answerExt, durationMi);
    }

    /**
     * 升级
     *
     * @author 老张
     */
    private void updateLevel(List<UserLevel> userLevels, AppUserExt uExt, BigDecimal durationMi) {
        AppUser appUser = apiAppUserMapper.selectById(uExt.getId());
        // 男性不需要判断视频认证即可积累通话时长并提升等级
        if (appUser.getSex().equals(SexEnum.女.getCode())){
            if (!appUser.getVideoAuth().equals(CheckStatus.审核通过.getCode())) {
                return;
            }
        }
        appUserExtMapper.updateById(
                new AppUserExt()
                        .setId(uExt.getId())
                        .setLevelExp(uExt.getLevelExp() + durationMi.intValue()));
        int duration = uExt.getCallDurationSum() + durationMi.intValue();
//        log.info("==============================================>用户的等级：{}", appUser.getLevel());
//        log.info("==============================================>用户的通话时常：{}", duration);
        for (UserLevel userLevel : userLevels) {
//            log.info("==============================================>当前等级规则：等级-{}，需要的分钟数-{}", userLevel.getLevel(),userLevel.getCallDurationMax());
//            log.info("==============================================>用户等级是否小于当前规则的等级：{}", appUser.getLevel() < userLevel.getLevel());
//            log.info("==============================================>用户的通话时常是否大于当前规则的时常：{}", duration >= userLevel.getCallDurationMax());
            if (duration >= userLevel.getCallDurationMax()
                && appUser.getLevel() < userLevel.getLevel()) {
//                log.info("==============================================>条件成立。");
                apiAppUserMapper.updateById(
                    new AppUser().setId(uExt.getId()).setLevel(userLevel.getLevel()));
                break;
            }
        }
    }

    /**
     * 创建话单
     *
     * @author 老张
     */
    private CallSheetInfo createCallSheetInfo(
            CallPayInfo callPayInfo,
            StartCallBo startCallBo,
            AppUser callUser,
            AppUser answerUser) {
        // 预先生成ucid
        String ucid =
                DateUtil.format(new Date(), DatePattern.PURE_DATETIME_PATTERN)
                        + callUser.getPlatformId();
        // 7.创建话单
        CallSheetInfo sheetInfo = new CallSheetInfo();
        sheetInfo.setType(startCallBo.getType());
        sheetInfo.setCallerId(callUser.getId());

        ServletRequestAttributes servletRequestAttributes =
                (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = servletRequestAttributes.getRequest();

        String branch = request.getHeader("branch");
        sheetInfo.setCallerPlat(Integer.parseInt(branch));
        sheetInfo.setUcid(ucid);
        sheetInfo.setPayUid(callPayInfo.getPayUid());
        sheetInfo.setBenefitUid(callPayInfo.getBenefitUid());
        sheetInfo.setAnswerId(startCallBo.getUid());
        sheetInfo.setCallPlatformId(callUser.getPlatformId());
        sheetInfo.setAnswerPlatformId(answerUser.getPlatformId());
        sheetInfo.setCallDate(DateUtil.format(new Date(), DatePattern.PURE_DATE_PATTERN));
        sheetInfo.setStatus(CallSheetStatus.等待接通.getCode());
        /** 被叫收益单价 */
        sheetInfo.setEarnPrice(callPayInfo.getUnitPrice());
        /** 通话单价 */
        sheetInfo.setCallPrice(callPayInfo.getUnitPrice());
        /** 主叫支付单价 */
        sheetInfo.setPayPrice(callPayInfo.getUnitPrice());
        /** 主叫支付费用 */
        sheetInfo.setPayCost(BigDecimal.ZERO);
        // 本次是否有免费通话分钟
        //        CallFreeBo callFreeBo = callIsFree(callPayInfo.getPayUid());
        //        sheetInfo.setFreeTimesTotal(callFreeBo.getRestM());
        //        sheetInfo.setFreeTimesRest(callFreeBo.getRestM());

        sheetInfo.setCallerPhone(callUser.getPhone());
        sheetInfo.setAnswerPhone(answerUser.getPhone());
        sheetInfo.setCallBranch(Integer.parseInt(branch));
        sheetInfo.setAnswerBranch(Integer.parseInt(branch));
        apiCallSheetInfoMapper.insert(sheetInfo);

        return sheetInfo;
    }

    /**
     * 检查是否可以呼出
     *
     * @author 老张
     */
    private void checkCallStatus(AppUser callUser, AppUser answerUser) {
        if (callUser.getSex().equals(answerUser.getSex())) {
            // 有舍有得 同性不能通话
            throw AppServiceException.paramException("账号数据异常");
        }
        // TODO 0.认证信息验证 没有实名认证不能提现但是不限制打电话和接电话,没有真人认证的只能8毛每分钟
        // TODO 1.挂断主叫未结束的电话
        // 3.官方账号或者平台账号不能打电话
        //        if (callUser.getAccountType().equals(AccountTypeEnums.官方.getCode())
        //                || callUser.getAccountType().equals(AccountTypeEnums.客服.getCode())) {
        //            throw AppServiceException.paramException("官方账号或者平台账号不能打电话");
        //        }
        // 4.测试账号只能打测试账号
        if (callUser.getAccountType().equals(AccountTypeEnums.测试.getCode())
                && !answerUser.getAccountType().equals(AccountTypeEnums.测试.getCode())) {
            throw AppServiceException.paramException("测试账号只能打测试账号");
        }
        // 5.检查被叫是否能打通
        //          1.检查被叫账户状态
        if (!answerUser.getStatus().equals(AppUserStatus.正常.getCode())) {
            throw AppServiceException.paramException("被叫账户异常");
        }
        // TODO 客户端自动挂断
        // traceUp#265 打视频需要判断用户状态，勿扰/离线/忙碌不可打
        if (!answerUser.getOnlineStatus().equals(AppUserOnlineStatus.空闲.getCode())) {
            throw AppServiceException.paramException(
                    "被叫账户" + AppUserOnlineStatus.getByCode(answerUser.getOnlineStatus()).name());
        }
        if (answerUser.getDisturbFlag().equals(EnableEnum.YES.getCode())) {
            throw AppServiceException.paramException("被叫开启了勿扰");
        }
        UserBlackList userBlackList =
                userBlackListMapper.selectOne(
                        Wrappers.lambdaQuery(UserBlackList.class)
                                .eq(UserBlackList::getToUserId, callUser.getId())
                                .eq(UserBlackList::getFromUserId, answerUser.getId()));
        if (userBlackList != null) {
            throw AppServiceException.inBlackList("已经被拉黑");
        }
        CallSheetInfo callSheetInfo =
                apiCallSheetInfoMapper.selectOne(
                        Wrappers.lambdaQuery(CallSheetInfo.class)
                                .eq(CallSheetInfo::getStatus, CallSheetStatus.正在通话.getCode())
                                .and(
                                        item ->
                                                item.eq(
                                                                CallSheetInfo::getCallerId,
                                                                callUser.getId())
                                                        .or()
                                                        .eq(
                                                                CallSheetInfo::getAnswerId,
                                                                callUser.getId())),
                        false);
        if (callSheetInfo != null) {
            throw AppServiceException.paramException("您还有未结束的话单,请联系客户结束话单");
        }
    }

    /**
     * 检查主叫或者被叫的余额
     *
     * @author 老张
     */
    private CallPayInfo checkBalance(
            AppUser callUser, AppUser answerUser, StartCallBo startCallBo) {
        log.info("呼叫：" + callUser.toString());
        log.info("接听" + answerUser.toString());
        // 本次支付的单价
        BigDecimal price = BigDecimal.ZERO;
        // 付款方的uid
        AppUser payUser;
        // 受益人uid
        AppUser benefitUser;
        // 20240927 有舍有得 要求改成都是男用户扣钱
        if (startCallBo.getType().equals(StartCallType.随机视频.getCode())
            || startCallBo.getType().equals(StartCallType.定向视频.getCode())) {
            // 2024年08月28号 有舍有得 视频速配不管是男匹配女还是女匹配男都是男付款
            if (callUser.getSex().equals(SexEnum.男.getCode())) {
                log.info("主叫付费");
                // 主叫是男号,单价取女号的,即被叫
                price = answerUser.getVideoPrice();
                // 付款方是男号,即主叫
                payUser = callUser;
                benefitUser = answerUser;
            } else {
                log.info("被叫付费");
                // 主叫是女号,单价取女号,即主叫
                price = callUser.getVideoPrice();
                // 付款方是男号,即被叫
                payUser = answerUser;
                benefitUser = callUser;
            }
        }
        //        else if (startCallBo.getType().equals(StartCallType.定向视频.getCode())) {
        //            // 价格是被叫的价格
        //            price = answerUser.getVideoPrice();
        //            payUid = callUser.getId();
        //            benefitUid = answerUser.getId();
        //        }
        else if (startCallBo.getType().equals(StartCallType.客服视频.getCode())) {
            price = answerUser.getVideoPrice();
            payUser = callUser;
            benefitUser = answerUser;
        } else {
            throw AppServiceException.paramException("通话异常~");
        }

        // 本次通话是否免费
        if (payUser.getSex() == SexEnum.男.getCode()) {
            AppUserVip appUserVip = appUserVipMapper.selectById(payUser.getId());
            Boolean isVip =
                appUserVip.getCommonVipFlag().equals(EnableEnum.YES.getCode())
                    && appUserVip.getCommonVipExpireTime().getTime() >= new Date().getTime();
            String strFreeTimes = sysParamService.selectConfigByKey(SysConfigConsts.男VIP每日免费通话时间);
            if ((!StrUtil.isEmpty(strFreeTimes)) && isVip) {

                Integer times = Integer.parseInt(strFreeTimes);
                Object cacheObject =
                    RedisUtils.getCacheObject(CacheConstants.LIMIT_MALE_FREE_CALL_DAY + payUser.getId());
                // 已经使用的分钟数
                Integer useM = (Integer) cacheObject;
                // 剩余的分钟数
                int restM = useM == null ? times : times - useM;
                log.info(
                    "checkBalance,男号每日免费分钟数,{},已经免费分钟数,{},剩余免费分钟数:{},付费人ID:{}",
                    times,
                    cacheObject,
                    restM, payUser.getPlatformId()); //   return !isFree;
                if (restM > 0) {
                    return CallPayInfo.builder().unitPrice(price).benefitUid(benefitUser.getId()).payUid(payUser.getId()).build();
                }

            }
        }

        // 检查钱包信息
        BigDecimal goldBalance = appUserWalletMapper.selectById(payUser.getId()).getGoldBalance();
        if (NumberUtil.isLess(goldBalance, price)) {
            if (LoginHelper.getUserId().equals(payUser.getId())) {
                throw AppServiceException.goldNotEnoughException("您的余额不足,请充值后再试吧~");
            } else {
                throw AppServiceException.goldNotEnoughException("对方余额不足,请稍后再试吧~");
            }
        }
        return CallPayInfo.builder().unitPrice(price).benefitUid(benefitUser.getId()).payUid(payUser.getId()).build();
    }

    /**
     * 检查是否等待接通-
     *
     * @author 老张
     */
    private CallSheetInfo checkWaitCall(String ucid) {
        CallSheetInfo callSheetInfo =
                apiCallSheetInfoMapper.selectOne(
                        Wrappers.lambdaQuery(CallSheetInfo.class)
                                .eq(CallSheetInfo::getAnswerId, LoginHelper.getUserId())
                                .eq(CallSheetInfo::getUcid, ucid));

        if (callSheetInfo == null) {
            throw AppServiceException.paramException("话单异常");
        }
        if (callSheetInfo.getStatus() != CallSheetStatus.等待接通.getCode()) {
            throw AppServiceException.paramException(
                    CallSheetStatus.getByCode(callSheetInfo.getStatus()).name());
        }
        return callSheetInfo;
    }

    /**
     * 未接听通知
     *
     * @author 老张
     */
    private void notifyNoAnswer(CallSheetStatus status, CallSheetInfo callSheetInfo) {
        Date date = new Date();
        callSheetInfo.setStopTime(date);
        Long fromPlatFormId;
        List<Long> toPlatFormId;
        if (status.equals(CallSheetStatus.未接通系统挂断)) {
            fromPlatFormId = SysIdConstant.系统;
            toPlatFormId =
                    ListUtil.toList(
                            callSheetInfo.getAnswerPlatformId(), callSheetInfo.getCallPlatformId());
            // 通知主叫和被叫
        } else if (status.equals(CallSheetStatus.未接通被叫挂断)) {
            fromPlatFormId = callSheetInfo.getAnswerPlatformId();
            toPlatFormId = ListUtil.toList(callSheetInfo.getCallPlatformId());
            // 被叫通知主叫 2：未接通被叫挂断
        } else {
            // 主叫通知被叫 1：未接通主叫挂断 3：接听超时 4：被叫忙碌
            fromPlatFormId = callSheetInfo.getCallPlatformId();
            toPlatFormId = ListUtil.toList(callSheetInfo.getAnswerPlatformId());
        }

        AppUserExt appUserExt = appUserExtMapper.selectById(callSheetInfo.getAnswerId());
        // 通话失败+1 npm config set registry https://registry.npmmirror.com/
        appUserExtMapper.updateById(
                new AppUserExt()
                        .setId(callSheetInfo.getAnswerId())
                        .setCallFail(appUserExt.getCallFail() + 1));

        CallOperationEnum.挂断通话
                .getApply()
                .apply(
                        CallInfo.builder()
                                .duration(0L)
                                .fromPlatFormId(fromPlatFormId)
                                .toPlatFormId(toPlatFormId)
                                .ucid(callSheetInfo.getUcid())
                                .price(callSheetInfo.getCallPrice())
                                .build());
        AppUserExt callAppUserExt = appUserExtMapper.selectById(callSheetInfo.getCallerId());
        if (!status.equals(CallSheetStatus.未接通被叫挂断)) {
            PushMessage.sendLinkMsg(
                    ListUtil.of(callSheetInfo.getAnswerPlatformId()),
                    "刚才用户" + callAppUserExt.getNickName() + "想要与您通话，您未能接听。快去看看吧～～【点击立即查看】",
                    "chat/single?userId=" + callSheetInfo.getCallerId());
        }
    }

    /**
     * 获取剩余通话分钟数
     *
     * @author 老张
     */
    private Integer getRestMinute(Long uid, BigDecimal callPrice) {
        BigDecimal goldBalance = appUserWalletMapper.selectById(uid).getGoldBalance();
        return goldBalance.divide(callPrice, 0, RoundingMode.DOWN).intValue();
    }
}
